/**
 *
 */
package org.jprobe.reflect.classpath;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * <p>an implementation of ClassPathInterface, this is just a data structure which
 * holds information about classes loaded in the classpath, to populate entries
 * in this object, an implementation of <code style='color:#23F'>ClassPathReaderInterface</code>
 * has to be used. Alternatively, you may use the static method
 * <code style='color:#A3F'>ClassPathReader.<em>getClassPath</em>()</code> which returns a single
 * copy of the populated classpath and cares for the system resources, because
 * there are thousands of classpath entries.</p>
 *
 * @author Hussain Al-Mutawa
 * @version 1.0
 */
public abstract class AbstractClassPath implements ClassPathInterface {
	/** tracks how many instances of this object are in the memory **/
	private static int instancesCounter=0;
	/** the packages in the classpath **/
	protected transient final Map<String,PackageInterface>packages;
	/** classpath entries **/
	protected transient final Set<String>entries;
	/** initializes a new abstract classpath object **/
	public AbstractClassPath() {
		super();
		if(instancesCounter>0){
			throw new UnsupportedOperationException("This object has been already initialized, use one copy only");
		}
		this.packages=new HashMap<String,PackageInterface>();
		this.entries=new HashSet<String>();
		instancesCounter++;
	}

	/**
	 * gets the path to the java runtime library
	 * @return physical path in the file system for java runtime library
	 */
	public abstract String getJavaRuntimeLibrary();
	/**
	 * specifies the classloader used to load classes, this should help prevent 
	 * having multiple class loader and makes manging it easier
	 * @return url class loader
	 */
	public abstract ClassLoader getClassloader();

	@Override
	public Set<String> getPackages() {
		return packages.keySet();
	}

	@Override
	public Set<String> getEntries() {
		return this.entries;
	}

	@Override
	public Class<?>[] getClasses(final String packageName) throws ClassNotFoundException{
		final Set<String>set=getPackage(packageName).getClasses();
		final Class<?>[]classes=new Class<?>[set.size()];
		final Iterator<String>it=set.iterator();
		int i=0;
		while(it.hasNext()){
			//System.out.println(packageName+'.'+it.next());
			classes[i++]= getClassloader().loadClass(packageName+'.'+it.next());

		}
		return classes;
	}

	

	@Override
	public void addPackage(final PackageInterface packageInterface) {
		this.packages.put(packageInterface.getName(),packageInterface);
	}

	@Override
	public void addEntry(final String entry) {
		this.entries.add(entry);
	}

	@Override
	public boolean hasClass(final String classBinaryName) {
		if(!classBinaryName.contains(".")){
			throw new IllegalArgumentException("class ["+classBinaryName+"] is not included in a pacage");
		}
		final String pkg=classBinaryName.substring(0,classBinaryName.lastIndexOf("."));
		if("".equals(pkg)){
			throw new IllegalArgumentException("class ["+classBinaryName+"] is not included in a pacage");
		}
		final String cls=classBinaryName.substring(classBinaryName.lastIndexOf(".")+1);
		if("".equals(cls)){
			throw new IllegalArgumentException("class ["+classBinaryName+"] is not included in a pacage");
		}
		return packages.containsKey(pkg) && packages.get(pkg).getClasses().contains(cls);
	}

	@Override
	public PackageInterface getPackage(String name) {
		final PackageInterface result = packages.get(name);
		//System.out.println(result);
		return result;
	}

	@Override
	public boolean isRuntimeClass(final Class<?>clazz){
		Class<?>aclazz=clazz;
		while(aclazz.isArray()){
			aclazz=aclazz.getComponentType();
		}
		return aclazz.isPrimitive() ||
			(
				getPackages().contains(aclazz.getPackage().getName())
			 && getPackage(aclazz.getPackage().getName()).getClasses().contains(aclazz.getSimpleName())
			 && getJavaRuntimeLibrary().equals(getPackage(aclazz.getPackage().getName()).getClasspath())
			);
	}

	@Override
	public Class<?> getDirectSubClass(final Class<?> clazz) {
		final String pkg=clazz.getPackage().getName();
		if(!getPackages().contains(pkg)){
			throw new IllegalArgumentException("Package ["+pkg+"] is not in the classpath");
		}
		final Set<String>classes=getPackage(pkg).getClasses();

		Class<?>result=null;
		for(String aclazz:classes){
			try{
				final Class<?>subtype=getClass().getClassLoader().loadClass(pkg+'.'+aclazz);
				if(isSubClass(clazz, subtype)){
					result = subtype;
					break;
				}
			}catch(final ClassNotFoundException cnfe){/* do nothing, it could be a private anonymous class */}

		}
		return result;
	}
	
	@Override
	public void removeClassPathEntry(final String classpath){
		final List<String>_packages=new ArrayList<String>();
		for(final PackageInterface _package : this.packages.values()){
			if(_package.getClasspath().equals(classpath)){
				_packages.add(_package.getName());
			}
		}
		for(final String pkg:_packages){
			if(this.packages.remove(pkg)==null){
				throw new IllegalStateException("package ["+pkg+"] can not be removed");
			}
			//System.out.println(pkg);
		}
		this.entries.remove(classpath);
	}

	@Override
	public List<Class<?>>getSubClasses(final Class<?>clazz) {

		final String pkg=clazz.getPackage().getName();
		if(!getPackages().contains(pkg)){
			throw new IllegalArgumentException("Package ["+pkg+"] is not in the classpath");
		}
		final Set<String>classes=getPackage(pkg).getClasses();

		final List<Class<?>>list=new LinkedList<Class<?>>();
		for(String aclazz:classes){
			try{
				final Class<?>subtype=getClass().getClassLoader().loadClass(pkg+'.'+aclazz);
				if(isSubClass(clazz, subtype)){
					list.add(subtype);
				}
			}catch(final ClassNotFoundException cnfe){/* do nothing, it could be a private anonymous class */}

		}
		return list;
	}

	@Override
	public boolean isSubClass(final Class<?>clazz,final Class<?>subtype){
		return 	clazz!=null && subtype!=null && clazz!=subtype
			&&	getAllSuperClasses(subtype).contains(clazz);
	}
	@Override
	public List<Class<?>> getSuperClasses(final Class<?> classObject){
		return getSuperClasses(classObject,false,false);
	}
	@Override
	public List<Class<?>> getSuperClasses(final Class<?> classObject,final boolean includingSelf,final boolean includingObject) {
		final Set<Class<?>>set=getAllSuperClasses(classObject);
		if(!includingObject){
			set.remove(classObject);
		}
		if(!includingSelf){
			set.remove(Object.class);	//to avoid redundancy because every class is a sub-class of Object.class
		}
		return Arrays.asList(set.toArray(new Class<?>[set.size()]));
	}
	/**
	 * Builds an <b>unordered</b> set of all interface and object classes that
	 * are generalizations of the provided class.
	 *
	 * @param classObject the class to find generalization of.
	 * @return a Set of class objects.
	 */
	private Set<Class<?>> getAllSuperClasses(final Class<?> classObject) {
		final Set<Class<?>> generalizations = new HashSet<Class<?>>();
		generalizations.add(classObject);

		final Class<?> superClass = classObject.getSuperclass();
		if (superClass != null) {
			generalizations.addAll(getAllSuperClasses(superClass));
		}

		generalizations.addAll(Arrays.asList(getSuperInterfaces(classObject.getInterfaces())));

		for (final Class<?> superInterface : classObject.getInterfaces()){
			generalizations.addAll(getAllSuperClasses(superInterface));
		}

		return generalizations;
	}



	@Override
	public Class<?>[] getSuperInterfaces(final Class<?>[] childInterfaces) {

		final List<Class<?>> allInterfaces = new ArrayList<Class<?>>();

		for (int i = 0; i < childInterfaces.length; i++) {
			allInterfaces.add(childInterfaces[i]);
			allInterfaces.addAll(
					Arrays.asList(
							getSuperInterfaces(childInterfaces[i].getInterfaces())));
		}

		return (Class[]) allInterfaces.toArray(new Class[allInterfaces.size()]);
	}

	/**
	 * gets the class <a href='http://docs.oracle.com/javase/6/docs/api/java/lang/ClassLoader.html#name'>binary name</a> including the package name then the class name,
	 * e.g:
	 * <pre>
	 * 		"java.lang.String[]"
	 * 		"java.util.HashMap"
	 * 		"java.lang.String"
	 * 		"javax.swing.JSpinner$DefaultEditor"
	 * 		"java.security.KeyStore$Builder$FileBuilder$1"
	 * 		"java.net.URLClassLoader$3$1"
	 * </pre>
	 * @param type the class from which the binary name iis extracted
	 * @return class binary name
	 */
	public static String getClassBinaryName(final Class<?>type){
		String result;
		if(type==Void.TYPE){
			result = "void";
		}else if(type.isArray()){
			result = getClassBinaryName(type.getComponentType())+"[]";
		}else if(type.isPrimitive() || type.getPackage()==null){
			result = type.getSimpleName();
		}else{
			result = type.getName().replace('$', '.');
		}
		return result;
	}
	
	
}
